Skip to content

feat(settings): support custom font families and weights - #1074

Merged
agegr merged 3 commits into
agegr:mainfrom
TwinklerG:feat/custom-font-family-weight
Oct 8, 2026
Merged

agegr merged 3 commits into
agegr:mainfrom
TwinklerG:feat/custom-font-family-weight

Conversation

@TwinklerG

@TwinklerG TwinklerG commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1073

Summary

Adds custom font families and base weights to Settings → General, in a new Fonts & layout section that also holds the existing chat font size and chat content width.

  • Interface and monospace fonts are set separately. Each takes a comma-separated list of font names. The monospace font covers code, tool output, diffs and the terminal.
  • Each font row has the same shape as the size and width rows: label, font-name field, weight value and reset on one line, and a weight slider under it. The weight runs from 100 to 600 in steps of 100, default 400. Values, resets and sliders line up across all four rows, and both name fields share one grid so they also line up across locales.
  • The name field is drawn in the font it names, so it is also the live preview.
  • The Chat section keeps only its behavior switches: expand thinking, selection actions, send key.
  • Preferences are saved in the current browser only. They are restored on startup and synced across tabs; chat width and font size are not affected.
  • Only fonts installed on the browser's device are used. Nothing is uploaded or downloaded, and the default stacks stay as fallbacks. Font names are always treated as names and quoted, never applied as CSS.
  • Headings and emphasis keep their own weights. Base weights stop at Semi Bold (600) because headings and labels use 600, and bold text, highlighted bold and the terminal's bold use 700; a heavier base would outweigh the text meant to stand out.
  • The monospace weight applies to code, source and diff views. Changing the interface font remeasures an existing chat draft.
  • Changing a font updates and refits open terminals without restarting their shells or SSE streams.
  • English, Simplified Chinese and Traditional Chinese labels, a topic note in docs/agents/settings-ui.md, unit tests and browser regression coverage are included.

Validation

Run on this branch merged with the current main:

  • node_modules/.bin/tsc --noEmit
  • npm run lint
  • npm test: 2406 passed
  • npm run test:e2e: font families and weights, persistence, cross-tab sync, independent resets, preserved emphasis and draft resizing, at 1280px and 390px. The main suite has a pre-existing intermittent failure at the sidebar toggle after the chat appearance check (e2e/run.mjs ~line 453), which also occurs on plain main. It is a test race with the effect that restores the desktop sidebar.
  • npm run test:terminal: live terminal font and weight updates without shell or SSE restarts, at 1440px and 390px
  • Measured in the browser at 1280px (English and Chinese) and 390px: both name fields start at the same x, all four resets and sliders share one column and width, and there is no horizontal overflow.

Maintainer follow-up

Two commits were added on top of the original one:

  • refactor(settings): the font controls moved from under the theme picker into the Fonts & layout section, with the shared row shape described above. The separate preview lines and the long per-control descriptions were removed, and the labels were shortened.
  • fix(settings): base weights are capped at 600 so emphasis stays heavier. The terminal's document.fonts refit was removed: xterm ignores an option set to its current value, so it never remeasured.

TwinklerG and others added 3 commits October 5, 2026 21:01
Add separate interface/chat and monospace font preferences with live previews, browser persistence, fallback stacks, independent resets, and cross-tab synchronization. Update code rendering, draft sizing, and terminal fonts without restarting terminal sessions; add unit and browser regression coverage.
… the sliders

Fonts, chat font size and content width move into one section after
Appearance; Chat keeps its behavior switches. Each row is a header line
(label, value, reset) over a full-width slider: a font row puts its name
field in the header and uses the slider for its weight, so values,
resets and sliders line up, and both name fields share one grid. The
name field is drawn in the font it names, replacing the separate preview
lines; labels and the description are shortened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…no-op font-load refit

Base weights stop at Semi Bold (600). Headings and labels use 600 and
bold text, highlighted bold and the terminal's bold use 700, so an 800 or
900 base made ordinary text heavier than the text meant to stand out.
xterm's default bold (700) now always stays above the base weight.

The terminal's document.fonts ready/loadingdone refit did nothing: xterm
ignores an option set to its current value, so it never remeasured and
only ran fit() whenever any font on the page loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@agegr
agegr merged commit 86d94e2 into agegr:main Oct 8, 2026
2 checks passed
dreadster3 added a commit to dreadster3/pi-web that referenced this pull request Oct 9, 2026
* fix(subagents): reject unsupported resume option overrides

Reject effective creation options before resuming an existing child. Preserve empty defaults, including max_turns: 0, and document the continuation contract. Reuse the existing SDK integration harness to verify rejection before dispatch and compatible continuation.

* Render write() tool content as readable file text (agegr#1024)

* Render write tool content as readable file text

(cherry picked from commit 8476b85b6579054a61a7da97592deb0eb7646dd0)

* fix(chat): keep write's JSON view when the file text would hide arguments

Show the file text only for a write whose input is just path/file_path and
content, so another argument (a mode, a title) stays visible. Streamed input,
which is still incomplete JSON, and an empty file keep the generic view too.
Drop the duplicate tool-call-expansion import the merge with main left in the
test file, which stopped MessageView.test.mjs from loading.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Alex Yang <agegcn@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* feat(chat): show extension command buttons in the status bar (agegr#1030)

* feat(chat): render extension action chips in the composer footer

Extensions can declare a clickable action chip via a status entry whose
key starts with "chip:": ctx.ui.setStatus("chip:/mode toggle", "🔨 Build").
The chip renders in the composer footer to the left of the reasoning-level
control (directly visible on mobile too), and clicking it sends the command
part of the key through the normal send path. Extension commands execute
immediately server-side and are not recorded as user messages, so the chip
acts as a lightweight remote control (mode toggles, quick prompts).

- chip statuses are hidden from the bottom extension status shelf to
  avoid duplication
- chips are disabled while the session is streaming, matching other
  footer controls

* feat(chat): show extension command buttons in the status bar

Move the action buttons from the composer footer into the extension status
bar, where setStatus text already lives, and rename the convention:
ctx.ui.setStatus("command:/mode toggle", "Build") is a button cell that sends
/mode toggle.

- Only keys whose command starts with "/" make a button. Anything else would
  reach the model, and "!" would run a shell command; those stay plain text.
- Every status is its own cell, in key order, divided by vertical rules like
  the widget triggers. A command cell is a button as tall as the bar with no
  box border, so the whole cell is clickable. Statuses with nothing visible
  get no cell. The text is cleaned of ANSI and whitespace as before.
- The widget triggers and the status line share one row that scrolls sideways
  as a whole; the expanded widget panel stays above it, outside the scroll.
- Buttons are disabled while the session is busy. Touch screens get 44px cells
  (widget triggers included) when the bar has a button.
- Document the convention in docs/agents/sessions.md and register the two
  components in AGENTS.md.

The ChatInput footer chips and their source-regex test are dropped.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: ddonggit <ddonggit@users.noreply.github.com>
Co-authored-by: Alex Yang <agegcn@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(chat): let extension dialogs and the custom panel be widened (agegr#947) (agegr#1032)

* fix(chat): let extension dialogs and the custom panel be widened (agegr#947)

Extensions showing long content - a formatted SQL confirmation, a diff, a
read-only side panel - were boxed into the hardcoded min(560px, 100%)
dialog with no way to get more room, and every wrapped line also costs
height (agegr#947).

- ctx.ui select/confirm/input/editor accept an optional dialogSize
  ("sm" | "md" | "lg" | "full") hint. "sm" keeps the historical 560px
  dialog, so default behavior is unchanged; unknown values fall back to
  it instead of poisoning the request.
- The extension dialog and the custom panel get a maximize/restore
  button next to the collapse chevron. "full" fills the content region
  above the composer while the overlay keeps its own 20px breathing
  room, so the composer stays visible.
- Without a request-level hint the user's maximize choice persists in
  localStorage across openings; a hint wins over the stored preference,
  and toggling a hinted dialog never writes back into it.

* fix(chat): fit extension dialogs to their content instead of taking a size hint (agegr#947)

Drop the dialogSize option from ctx.ui.{select,confirm,input,editor}. It was a
pi-web-only field that no stock pi extension sends, and the problem is purely
how the dialog is shown, so the browser should decide.

A dialog now opens at its usual 560px and grows only when a code block or table
inside it would scroll sideways (formatted SQL, diffs, logs): it takes exactly
the missing width, rounded up to 40px, capped at the content region above the
composer. Prose wraps and never triggers it. It only grows, and re-measures when
highlighted code replaces its plain fallback.

The maximize/restore button stays as the user's override for the dialog on
screen. Nothing is stored: a later dialog starts from its own content.

The custom panel's maximize button is gone. Its lines are already wrapped to the
width the extension asked for, so widening the box never changed the wrapping.
The panel is now as wide as its widest line (at least its usual 920px, at most
the content region), so lines wider than 920px no longer scroll sideways.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: yanshinan <yanshinan.js@chinatelecom.cn>
Co-authored-by: Alex Yang <agegcn@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(agents): extensions must behave the same here as in the pi CLI

State the principle behind agegr#1032 in AGENTS.md: display problems are fixed in the
web client, never with pi-web-only options or fields on the extension API, and
pi's TUI is the reference for what an extension's request means.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* perf(sidebar): replace SMIL spinner with compositor-friendly CSS animations (agegr#1042)

The running-session spinner and unread-activity ripple used SMIL
<animateTransform>/<animate> with repeatCount=indefinite. SMIL geometry
animations force a main-thread repaint of the whole page every frame
while any agent is running (measured 12-19% GPU on RTX 2080 at
2560x1080 vs ~2% with the animation paused, A/B/A verified reversible).

- RunningSessionIndicator + project activity badge: SVG <animateTransform>
  -> Tailwind animate-spin (CSS transform rotate, already used by
  AgentSessionPanel/AppShell spinners). Same 14px/10px arc path.
- UnreadSessionIndicator: SMIL r/opacity ripple -> .unread-ping CSS
  opacity-only keyframes, with prefers-reduced-motion fallback.

Co-authored-by: userliwq <userliwq@users.noreply.github.com>

* fix: preserve failed uploads and authorize git diff targets (agegr#1039)

Fixes agegr#1038 and agegr#1037. Add regression coverage for failed replacements, junction access, and deleted-file diffs.

Co-authored-by: Elvis <linrenhao@gmail.com>

* fix(models): preserve the catalog API protocol for models-only providers (agegr#1050)

* fix(models): preserve catalog protocol for models-only providers

* test(e2e): restore desktop state before model discovery

---------

Co-authored-by: jidekaixin2dian <322988850+jidekaixin2dian@users.noreply.github.com>

* feat(subagents): preload named skills from profiles (agegr#1034)

* feat(subagents): preserve named skill scopes in profiles and snapshots

* feat(subagents): preload named skills across spawn resume and reopen

* fix(subagents): keep profiles readable and match pi's skill switch

- Parse `skills:` leniently. An empty value, a trailing comma or an empty or
  non-string item made the profile throw, so it vanished from the Agents
  panel and `Agent` reported it unknown. Empty and non-string items are now
  dropped, as pi-subagents does, and an empty value means no list.
- `true` / `all` / `none` / `false` stay switch spellings that a save keeps
  in step with `load_skills`, as on main; `skills: none` alone switches
  skills off.
- `load_skills: false` is pi's `noSkills` again, which keeps the skills an
  extension provides; the override no longer hides them.
- A malformed snapshot list narrows to the names it holds instead of
  throwing, so session reads and reload keep working; it never widens to the
  catalog. Drop the validate-only call before resume.
- Drop the name-pattern check: names are only looked up among discovered
  skills, and it could only block real ones.
- Return nothing from before_agent_start when there is no text to add.
- Show a profile's skills list read-only under the skills switch.
- Move the notes into docs/agents/subagents.md and fix the stale
  replace-mode sentence in docs/agents/tools.md. Replace mode without a list
  appending pi's catalog is kept and documented, as pi's --system-prompt does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Alex Yang <agegqqq@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(subagents): load only the extensions a profile's extensions: list names (agegr#1091)

A subagent profile's `extensions:` key was read as a switch only: a list such
as `extensions: [codegraph]` loaded every extension, and so did
`extensions: none`. pi-subagents, which reads the same profile files, loads
only the listed extensions.

- `scopeSubagentExtensions()` is an `extensionsOverride` that keeps the named
  extensions after the SDK loaded them, as pi-subagents does, and drops the
  provider, native provider and virtual model registrations the others
  queued. Dropped extensions bind no handlers, tools or commands; their
  factory has already run once, so this is a scope, not a sandbox. It uses
  only public loader options, and the SDK reapplies it on every reload.
- Names resolve like `ext:` selectors (directory, file or package name,
  case-insensitive); a name two sources claim keeps neither. pi-web's own
  `<inline:...>` extensions always stay. Path entries load nothing, so a
  project profile cannot load code that project trust gates.
- `*` keeps every extension, `none` / `false` switch extensions off, and
  `load_extensions: false` wins over a list. The list parses as leniently as
  `skills:`.
- The child's snapshot keeps the names, so resume and RPC reopen apply the
  same scope (`subagentExtensionLoaderOptions()`, shared by both paths).
- The Agents panel shows the list read-only under the extensions switch, and
  a save keeps it as authored. The skills line now says it lists skills, since
  the two lines sit side by side.

Refs agegr#1035, agegr#955

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(markdown): wrap long table cells without squeezing wide tables (agegr#1056)

Bound header and data cells while preserving natural table sizing and horizontal scrolling. Cover long headings, prose, links, inline code, and wide tables in the existing desktop and mobile browser regression.

Fixes agegr#1052.

Co-authored-by: wayne.luo <wayne.luo@bybit.com>

* fix(subagents): keep turn limits and terminal outcomes at the SDK boundary (agegr#1093)

* fix(subagents): preserve terminal outcomes at turn limits

Use the awaited SDK finishTurn boundary for bounded runs, preserve natural completion and finalized tool termination, and retain partial output for budget failures. Keep cancellation and provider failures distinct.

* fix(subagents): keep the turn limit when messages are queued

Follow-up to the turn-limit lifecycle change from agegr#1054:

- An `end` decision from finishTurn did not stop the run: AgentSession
  starts another run for anything still queued. A parent steer during the
  wrap-up turn led to a third model call with max_turns: 1. The limiter
  now clears the queues, lists those messages in the turn-limit error,
  and refuses steer_subagent from then on.
- In one-at-a-time mode a steer queued before the wrap-up turn took that
  turn and held the instruction back, so a final answer was reported as a
  turn-limit failure. The wrap-up turn now takes everything queued.
- Stop in the child's own chat ends the request with stopReason
  "aborted" without telling the controller; report it as aborted instead
  of failed, on start and resume.

The test that claimed queued messages were kept for resume passed only
because the extra call consumed them; it now checks they are reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: wayne.luo <wayne.luo@bybit.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(markdown): preserve code backgrounds across theme changes (agegr#1060)

* fix(markdown): preserve code backgrounds across theme changes

* fix(demo): mirror the code block background fix

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Alex Yang <agegcn@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(chat): refresh context usage between model calls (agegr#1058)

* fix(chat): refresh context usage between model calls

Change-Id: I423982a12d024afebc8da1e67836722a6e2aae3b

* fix(chat): keep an older usage read when a newer one fails

The shared request id let any newer usage read invalidate older ones, even
when the newer read failed (non-ok reply or network error). A good reply
from an in-flight poll was then dropped, and usage stayed stale until the
next assistant message, poll tick or agent_end.

Track the last applied id instead: a reply applies only when it is newer
than the last one applied, so delayed replies still cannot overwrite newer
usage. Each mount marks the reads started before it as stale, which keeps
the unmount invalidation without reading a ref in effect cleanup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Alex Yang <agegcn@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(shell): restore desktop sidebar state after mobile resizing (agegr#1059)

Change-Id: I3cd96254c43ddb59da17f60bd0e3d4e19de914f8

* test(e2e): wait for the desktop sidebar to settle after mobile resizing

Returning from the mobile breakpoint now restores the desktop sidebar
preference in an effect (agegr#1059), so the Show sidebar button could vanish
between the visibility check and the click.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* feat(chat): drop files onto the chat to upload them and mention them (agegr#1094)

Dropping a non-image file onto the chat did nothing. It now goes through
the explorer's existing upload (POST /api/files/<cwd>?type=upload) into
the working directory, and the composer gets an @mention per file.

- conflict=skip: a drop never replaces a file. A name that already
  exists is still mentioned (often the very file dragged out of the
  project) and a notice says the mention points to the existing file.
- Images still attach to the prompt; folders are left out with a notice,
  since the endpoint writes flat file names only.
- uploadFiles() moves from FileExplorer to lib/file-upload-client.ts so
  the explorer and the chat share one client.
- The explorer tree refreshes after a drop upload.

Lighter take on agegr#1065: no new endpoint, no folder copy, no renaming.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(models): list providers that extensions register at session_start (agegr#1071)

pi-web builds a fresh ModelRuntime for every model listing and never starts
a session in it. Extensions that register their provider only in the first
runtime of a process and defer later ones to session_start (pi-claude-bridge)
disappeared from the picker after the first listing, and a session could not
start on their models.

Remember the available models of every runtime pi-web builds and add back
providers a runtime does not register at all. A session whose requested or
restored model is deferred starts on the default and switches once its
extensions are bound.

Co-authored-by: Alex Yang <agegcn@gmail.com>

* fix(markdown): render emphasis next to CJK punctuation (agegr#1072)

CommonMark leaves the asterisks visible when a CJK sentence ends in
punctuation and the next sentence starts without a space.

Use remark-cjk-friendly's parse-only extension in the shared message and
preview pipelines. User messages inherit the same extension. Add coverage
for adjacent CJK sentences, brackets, nested emphasis, code and escapes.

The extension also tightens some existing ASCII and emoji boundaries,
which can leave outer asterisks visible in previously emphasized text.
This follows the extension's flanking rules and changes how those older
messages render.

Refs: commonmark/commonmark-spec#650, commonmark/commonmark-spec#839

* fix(sw): don't hold static responses behind the cache write (agegr#1089)

Opening Pi Web intermittently showed the service worker's offline page
("Pi Web is offline") after an upgrade, even though the local server was
perfectly healthy. Hard reloads sometimes recovered it, which made it look
like a flaky server.

The worker script URL is versioned (/sw.js?v=<app version>), so every
upgrade starts a cold cache: the first load misses on every
_next/static/* asset at once. cacheFirst() then awaited cache.put() before
returning the response, so the worker held every response until its write
landed. That congestion pushed navigations past NAVIGATION_TIMEOUT_MS
(8000), whose only fallback is offline.html.

Measured on one URL with one healthy server, only the worker differing:

  page controlled by the SW   8302 ms, and the offline page reproduced
  SW unregistered              716 ms
  after this change            652-2967 ms across 8 cold-cache navigations

Return the response first and keep the write alive with event.waitUntil. A
failed write now costs one network re-fetch instead of blocking the page.

Adds "a cache miss responds before the cache write finishes", which stubs
cache.put with a promise that never settles and asserts the response still
resolves; the fetch-event harness now supplies waitUntil.

* fix(sessions): don't force-navigate to an empty chat when the deleted session was already left (agegr#1083)

handleSessionDeleted captured selectedSession in its useCallback closure, so the
stale copy was still the deleted session when the DELETE request resolved after
the user navigated away, and it forced an empty chat. Read the selection from
selectedSessionRef (synced every render) and only take over when the session is
still selected at resolution time; its cwd is then correct for the new draft.

Co-authored-by: Alex Yang <agegcn@gmail.com>

* fix(security): rotate preview-mode secrets at startup to close proxy auth bypass

The published npm tarball ships .next/prerender-manifest.json, whose
preview.previewModeId is baked into every release. Next skips middleware
(proxy.ts) for requests whose x-prerender-revalidate header equals that id,
so anyone who reads the public tarball could bypass the password gate and the
Host/Origin/Fetch-Metadata checks on any route proxy.ts covers.

Rewrite the three preview secrets (id, signing key, encryption key) with fresh
random values in bin/pi-web.js before launching next start, so the published
values are useless and unique per launch. Done on the guaranteed launch path
rather than a postinstall hook so it can't be skipped with --ignore-scripts; a
read-only install warns instead of silently staying vulnerable.

Reported by XlabAI Team of Tencent Xuanwu Lab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(settings): support custom font families and weights (agegr#1074)

* feat(settings): support custom font families and weights

Add separate interface/chat and monospace font preferences with live previews, browser persistence, fallback stacks, independent resets, and cross-tab synchronization. Update code rendering, draft sizing, and terminal fonts without restarting terminal sessions; add unit and browser regression coverage.

* refactor(settings): one Fonts & layout section, every row shaped like the sliders

Fonts, chat font size and content width move into one section after
Appearance; Chat keeps its behavior switches. Each row is a header line
(label, value, reset) over a full-width slider: a font row puts its name
field in the header and uses the slider for its weight, so values,
resets and sliders line up, and both name fields share one grid. The
name field is drawn in the font it names, replacing the separate preview
lines; labels and the description are shortened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(settings): keep emphasis heavier than the base font weight; drop no-op font-load refit

Base weights stop at Semi Bold (600). Headings and labels use 600 and
bold text, highlighted bold and the terminal's bold use 700, so an 800 or
900 base made ordinary text heavier than the text meant to stand out.
xterm's default bold (700) now always stays above the base weight.

The terminal's document.fonts ready/loadingdone refit did nothing: xterm
ignores an option set to its current value, so it never remeasured and
only ran fit() whenever any font on the page loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Alex Yang <agegcn@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(chat): make file mentions undoable (agegr#1098)

* fix(chat): make file mentions undoable

* fix(chat): fall back to a plain edit when execCommand refuses a mention

Without the fallback a refused execCommand('insertText') silently dropped the
mention. setRangeText plus an input event keeps it landing, minus the undo entry.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Alex Yang <agegcn@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* feat(sidebar): project groups, pins and archive with a Sessions | Files split

The sidebar showed one project at a time in a 54px two-line list, shared its
height with the explorer, and offered no way to keep or hide sessions short of
deleting them. It now lists every project in one virtualized tree:

- A "Sessions | Files" tab row. Files holds the project dropdown, the worktree
  row and the explorer at full height; FileExplorer stays mounted while hidden.
  The vertical sessions/explorer split is gone.
- All projects as collapsible groups: the six newest families plus any running,
  unread or selected one, "show more", running/unread counts when collapsed.
  Groups only toggle; they never change the cwd. A group that loses "current"
  stays open, so rows do not jump under the pointer.
- Single-line rows (32px, 44px on phones) with a status slot, title, branch and
  short time; hover shows archive and a row menu (pin, rename, mark read/unread,
  archive, delete). Right-click dispatches pi-web:session-row-contextmenu first
  and opens the built-in menu only when nobody claims it. Phones get the menu
  as a bottom sheet kept inside the visual viewport.
- Global pinned section, pinned projects, archive with a 10 s undo toast, an
  archive view and an "Archived" tag in search. Bulk "archive older than 7
  days" per project, chunked to the 500-id request cap.
- Worktrees: clicking a session switches to its worktree; the Files tab row is
  the only explicit switch; a group "+" asks which worktree (or creates one)
  when the project has several.

Pins and archive times live in <agentDir>/pi-web-session-state.json, keyed by
family root id, never in .jsonl (custom entries would move the leaf and be
copied into forks; moving files breaks parentSession paths). Writes are queued,
locked and atomic with a revision that /api/agent/running reports so every tab
and device refetches. A family counts as archived while archivedAt >=
root.modified, so a new message (also from pi CLI resume) brings it back;
DELETE forgets the state. pi CLI's /resume still lists archived sessions.

AppShell.handleNewSession takes the target projectKey and adopts the project
before the cwd is reported, so starting a session in another project clears the
previous project's file tabs like every other project switch.

Tests: the new store, route, tree model, prefs, menu, toast and tree are
covered; tests that pinned the old split, row height and SessionItem are
rewritten; e2e selectors follow the new DOM. Docs: sessions.md,
files-and-access.md, client-platform.md, README context-menu section, AGENTS.md
file map. The unused lib/file-explorer-state.ts, the old subagent-nesting
lib/session-tree.ts and sessionsForProject are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(sidebar): page "show more" by 20 and restyle session rows

- "Show more" reveals 20 more sessions per click instead of the whole
  group; running, unread and selected sessions stay visible without
  using up the step. "Show less" appears once there is something to fold.
- Hover and selection are a rounded box inset from both edges, with the
  collapse chevron after the group name.
- The running spinner and the unread dot take the time's place on the
  right, so titles start further left.
- The list keeps the scrollbar's room while everything fits
  (scrollbar-gutter: stable, overflow-y: scroll before Safari 18.2), and
  the measured width keeps the left and right margins equal, so rows
  neither narrow nor shift when the list starts to scroll.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style(sidebar): 13px session titles, selection by color instead of weight

Titles go from 12px to 13px on desktop (phones already used 13px, so their
override is gone). Unselected titles take a softer mix of --text and the
panel color; the selected one takes the full --text color and no longer
turns semibold, as in the Claude Code sidebar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(file-viewer): keep the source view background across theme switches

The stock Prism themes color <pre> differently: `vs` with backgroundColor,
vscDarkPlus with background. The highlighted source view also set
background through customStyle, so switching between a light and a dark
theme made React remove one property beside the other on the same <pre>.
It warned about mixing shorthand and longhand, and the view lost its
background (transparent after switching to dark, white after switching
back to light).

As CodeBlock does since agegr#1060, the source view now sets backgroundColor
itself and uses vscDarkPlus without its <pre> background shorthand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(chat): pick the project and worktree above the new-session composer

A group's "+" in the sidebar no longer asks which worktree to use: it starts
the session at once, in the sidebar's worktree for the current project and
in the project root for another one.

The empty new-session page shows a quiet bar above the composer with the
project and, at the top of a git checkout, the worktree. The project menu
lists the sidebar's projects plus "Open another project…" (the folder
picker); the worktree menu lists the checkouts plus "New worktree…".
Picking one moves the fresh composer there: the draft (text and images)
and the chosen model and reasoning level go along instead of being parked
under the old folder. The sidebar keeps owning the cwd and project
identity; it reports what the bar shows and carries out its moves through
a small handle. The Files tab keeps its own project and worktree switcher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(sidebar): fork a session from its row menu

Fork (F) copies the session's current branch into a new session file whose
header points at the source, then opens it. It is offered in the project
groups, the pinned section and the archive view, also while the session
runs: POST /api/sessions/[id]/fork works on disk with its own
SessionManager (lib/session-fork.ts), takes the leaf the user sees (the
open wrapper's, else the file's), and never starts, prompts or shuts down
an AgentSession, so a running source keeps its run. Empty, unsaved and
subagent sources are refused with a toast.

The copy is selected like a row click and revealed in the tree through a
small reveal request that SessionTree handles once (it keeps the row
mounted until then and expires if the row never shows). If the user moved
on while the fork was being made, they stay put; the copy is marked
unread and the toast offers Open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(sidebar): keep the project order fixed and reorder projects by hand

Project groups no longer jump to the top when one of their sessions gets
new activity. The order is stored as projectOrder in
pi-web-session-state.json, shared by every browser and written through the
same lock, revision and optimistic queue as pins and archive:

- With nothing stored, the first sidebar that has the synced UI state and
  the full session details saves the current recent-activity order.
- A project seen for the first time is added at the top (add-projects
  never moves or evicts a stored key, so saving never moves anything on
  screen). Activity never moves a saved project again.
- Only a manual move changes the order: drag a group header (after a few
  pixels with a mouse, after a 350 ms long-press on touch or pen), or Move
  up / Move down in the group menu. A move is stored relative to one
  neighbour (move-project), so concurrent browsers do not overwrite each
  other. Pinned projects stay above the others; a drag stays in its band,
  a pin lands at the bottom of the pinned band and an unpin at the top of
  the rest.

The drag lives in hooks/useGroupDrag.ts: pointer events with capture, a
drop line computed from the tree model, a ghost outside the scroller,
auto-scroll near the edges, Escape to cancel, and a header-only
non-passive touchmove so list scrolling keeps working. A tap or click on a
header still toggles the group. The moved group is revealed with the
existing tree reveal request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style(chat): set the new-session project and worktree bar like the composer controls

The bar above the new-session composer now uses the composer's own
control style: the UI font for both buttons (the worktree no longer in
code type, in the bar or its menu), 12.5px, 30px high, 9px radius, muted
text that darkens on hover, dim icons, a thin divider between project and
worktree, and a little more room above the composer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(sidebar): name a fork after its source with a short random suffix

The sidebar's Fork now writes a name into the copy (and only the copy):
the title the sidebar shows for the source, " · ", and 4 random hex
digits, e.g. "PR#1030 状态栏命令按钮 · 3f9a". A long first-message title
is shortened to 40 code points first; forking a fork replaces its suffix
instead of stacking another. A source with no title text (only shell
output or images) gives an unnamed copy, as pi's /clone does. The chat's
fork-from-message and /clone keep pi's behaviour.

The row and the success toast cut the title before the suffix, never the
suffix, so copies stay distinguishable in the narrow sidebar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sidebar): keep the drag ghost clear of the drop line

While a project group is dragged, the floating name no longer covers the
blue drop line. It is a small one-line pill that follows the pointer (8px
above a mouse, 28px above a finger), placed by the pure ghostTopFor(): it
moves only as far as it must to stay clear of the pointer and the line and
inside the list, so it does not jump when the target changes. The line
gets a 6px dot at its left end so it reads as an insertion marker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(sidebar): share one project/worktree picker between the Files tab and the new-session bar

ProjectWorktreePicker holds the two buttons and both menus, and both places
use it: the new-session bar as two inline chips (unchanged look), the Files
tab as two full-width rows in the same type (no more code font): the
project name with its path cut from the left, and the worktree's branch,
"main" note and count.

The menus are SidebarMenu everywhere, so the Files tab gains the phone
bottom sheet and keyboard navigation. SidebarMenu gains a filter (from 8
choices, IME-safe, Escape clears then closes) and an optional secondary
action per item. Both project menus offer "Use default directory" and
"Open another project…"; both worktree menus offer "New worktree…"; only
the Files tab can remove a linked worktree (with the force confirmation
in the menu). Each place keeps its own behaviour through callbacks: a
Files pick moves the sidebar's cwd and parks the draft as before, a bar
pick moves the fresh composer and carries the draft.

The old inline-styled Files dropdowns and their state are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(chat): put the new-session project bar on the brand's row

The empty new-session page's header now holds the brand, the project and
worktree bar and the versions in one row. The bar stays beside the brand
while its chips fit whole and otherwise takes a line of its own under it,
with the full width below the floated versions: the room the names need
decides, not the viewport. The brand gives way beside the versions (the
update link's version is cut first) instead of dropping below them.

The inline picker's project chip loses its 60% cap: the bar is now as wide
as its chips, so the cap left a lone chip (a repo subdirectory, no worktree
list) with its name 0px wide. The project never shrinks; the worktree
button is cut first, down to its icons.

The app update check runs once per page, so a fresh composer's header has
the link in its first paint and a late link cannot push the bar down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(sidebar): keep the files tab's buttons in view, in the project card

The files tab is one card on the panel: the project and worktree rows,
then the six buttons used most (terminal, open in the file manager,
upload, refresh, file search, changes view), with no title row and no
line across the sidebar between the tabs and the tree. A short line
inside the card marks the buttons off from the rows.

The buttons spread from the rows' icon column to their chevrons and are
always the same ones: the changes view stays, disabled, while nothing is
changed, so no button moves when an agent's edit or a commit flips the
count. The folder's actions come first and the tree's two toggles last.
They are a group of their own ("File actions"), outside the picker's, and
give way together at the sidebar's 180px minimum. The file search field
loses its line too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(sidebar): search the files from the header's search button

The files card loses its search button: on the files tab the header's
search button opens the file search instead (its title, pressed state and
aria-controls follow), and elsewhere, or on a files tab without a folder,
it is the session search as before. The card keeps five buttons: terminal,
file manager, upload, refresh and the changes view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style(sidebar): flatten the files tab's card into the sidebar's chrome

The card under the Files tab, white with its own border, drew the eye
away from the tree. The picker rows and the five buttons now sit flat on
the panel like the tab row, as one more strip of the sidebar's chrome,
and the tab row's line moves under them on that tab, so one line still
runs between the tabs and the tree.

The buttons take the tab row's ⋯ style: borderless, dim, bg-selected on
hover. The folder's four start at the rows' icon column, and the changes
view sits at the right end, where ⋯ is on the Sessions tab. With the
tree's 4px sides, the icons line up with the tree's chevrons and the
chevrons with its status letters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(sidebar): one toolbar row for the sidebar, main's project and worktree boxes

The sidebar's brand and its tab row become one toolbar row in the cells
of the chat's top bar beside it: Sessions | Files, New and search, 36px
with its line so the two bars read as one. The chosen tab and the open
search take the bar's accent line. Labels give way where they do not fit
(measured, since their widths change with the language): New keeps its
+, then the tabs their icons. The view menu goes: collapsing and
expanding every group moves to a group's menu and to Alt+click on its
header; the archive is the tree's footer row.

The project and worktree picker looks as the files tab drew it on main,
in both places: grey boxes with the project's whole path and the
worktree's branch in code type, cut at their left. The files tab stacks
them over square, borderless file keys and a line to the tree; the
new-session header shows them side by side without the box. Their menus
on a desktop are main's dropdowns again: whole paths, "Custom path…",
the "New worktree…" form under the list, a dirty checkout's question in
its row. Phones keep the sheets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(css): drop the unread ping the redesigned sidebar no longer uses

main's compositor-friendly .unread-ping replaced the SMIL ripple of the
old sidebar's unread indicator. The redesigned sidebar has no such
indicator: its unread dot (.session-tree-unread, app/sidebar.css) already
pulses with transform and opacity only and stops under reduced motion,
so the rule had no element left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(deps): upgrade pi to 1.1.0

Bump @earendil-works/pi-agent-core, pi-ai, pi-coding-agent and pi-tui
from 1.0.0 to 1.1.0 (releases v1.0.1 to v1.1.0). pi no longer ships an
npm-shrinkwrap.json, so pi-ai, pi-agent-core and pi-tui are one copy
shared with pi-web. Adapt to the changes that reach pi-web:

- Project overrides of global MCP servers (pi 1.0.1): a project entry
  without command, url or type changes only enabled, exposure and
  toolExposure of the global server of its name. Settings › MCP lists it
  as that global server with the changes, refuses one the loader skips
  in its words, and marks the global entry overridden. A global server's
  "This project" row turns it on or off for the project alone
  (set-in-project), as /mcp does. Writes keep enabled: true and
  exposure: "codemode" in an override, byte for byte as the SDK's
  editor. Test, sign-in and sign-out connect the merged entry, the host
  records it under the override's row, and the trust dialog says what
  an override changes.
- MCP sign-in passes the flow's signal to signInMcpServer() (pi 1.1), so
  a cancel or expiry stops it at any step.
- The paste importer reads oauth.clientRegistration and ports the
  validator's cimd rules.
- A stopped run is not a finished one (agent_settled.aborted): no done
  sound, browser notification or "Task finished" push; prompt_done
  carries aborted.
- Tool cards show the execution time pi now records (durationMs), as the
  CLI's "Took", instead of a timestamp difference that counted the
  model's generation; thinking blocks use the response's durationMs.
- The Azure provider is now `azure`; its icon follows.
- The ToolLoadout test stub gains getPromptGuidelines().

Settings › MCP selects draw their own chevron, inset like their text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(settings): scroll the general page across the dialog's full width

The general page's scroll box was capped at 680px, so its scrollbar sat in
the middle of the dialog with blank space to its right. The page now fills
the dialog and scrolls there; its children keep the 600px content column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style(settings): drop the general page's title, which repeats the active tab

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(files): explorer switch to show Git-ignored files, dimmed with the reason (agegr#1092)

* feat(files): explorer switch to show Git-ignored files, dimmed with the reason

The explorer bar's eye switch (off by default, kept in localStorage) lists
directories with ?type=list&hidden=1. The route then also returns what Git
ignores and what the name-list fallback hides, marked hidden: ignored|excluded;
.git and .DS_Store stay out. The tree dims those entries, names the reason in
the tooltip, and lets entries inside a hidden directory inherit it.

* feat(sidebar): the ignored-files switch in the files tab's head

The explorer bar the switch lived in is gone with main's Sessions | Files
tabs. The switch is now a key in the files tab's head, next to the changes
view: the folder's four actions from the left, the tree's two views (what it
lists, then its changes) at the right end.

- One label, "Show ignored files", with aria-pressed, like the changes view;
  the "Hide ignored files" string goes.
- Kept per browser in lib/sidebar-prefs.ts (pi-web:sidebar-files-show-ignored)
  and restored in the mount effect with the tab, so the first client render
  still matches the server's HTML. lib/file-explorer-state.ts is gone on main.
- Six keys fit the sidebar's 180px minimum: min-width 26px -> 21px. The push
  to the right end is a margin on the eye: a wrapper around both views took
  their icons' width as its size and squeezed them at any sidebar width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Alex Yang <agegcn@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore(deps): bump next to 16.3.8 and apply npm audit fix

next 16.3.6 carried six advisories (SSG/ISR cache poisoning, dev MCP
endpoint disclosure, image optimizer SSRF, among others). The pinned
version is what npm users install, so the patch release matters.
npm audit fix also refreshes js-yaml, sharp, dompurify, source-map-js
and brace-expansion within their ranges; mermaid moves to 11.17.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Release v0.11.0

* docs(worktrees): document branch picker

* docs(worktrees): fix branch menu scope

---------

Co-authored-by: wayne.luo <wayne.luo@bybit.com>
Co-authored-by: draxxris <24984408+draxxris@users.noreply.github.com>
Co-authored-by: Alex Yang <agegcn@gmail.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: ssr_acl <cqrxdyl@gmail.com>
Co-authored-by: ddonggit <ddonggit@users.noreply.github.com>
Co-authored-by: nsyan <70687289+nsyan@users.noreply.github.com>
Co-authored-by: yanshinan <yanshinan.js@chinatelecom.cn>
Co-authored-by: userliwq <sinashall@gmail.com>
Co-authored-by: userliwq <userliwq@users.noreply.github.com>
Co-authored-by: Elvis <linrenhao0813@gmail.com>
Co-authored-by: Elvis <linrenhao@gmail.com>
Co-authored-by: jidekaixin2dian <jidekaixinyid@gmail.com>
Co-authored-by: jidekaixin2dian <322988850+jidekaixin2dian@users.noreply.github.com>
Co-authored-by: acniray <acni_ray@126.com>
Co-authored-by: Alex Yang <agegqqq@gmail.com>
Co-authored-by: Wayne <uvforce@gmail.com>
Co-authored-by: xuzijian2019 <xuzijian2019@gmail.com>
Co-authored-by: Guillaume Legrain <30684712+SoulKyu@users.noreply.github.com>
Co-authored-by: keeword <keeword@users.noreply.github.com>
Co-authored-by: 0xfa1e <125674197+0xfa1e@users.noreply.github.com>
Co-authored-by: Mitchell Kager <mitchellkager@gmail.com>
Co-authored-by: Liang Gong <145010810+TwinklerG@users.noreply.github.com>
Co-authored-by: Tiger <tigerluyan@gmail.com>
Co-authored-by: Matthias Kuchem <mail@MatthiasKuchem.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] 支持自定义字体(font-family)和字重(font-weight)

2 participants